/*
 * Copyright (c) 2022. China Mobile(SuZhou)Software Technology Co.,Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package service

import (
	"io/fs"
	"os"
	"os/user"
	"strconv"
	"strings"
	"syscall"

	"github.com/juicedata/juicefs/pkg/meta"
	"github.com/juicedata/juicefs/pkg/utils"
	"github.com/juicedata/juicefs/pkg/vfs"
	cmap "github.com/orcaman/concurrent-map"
)

const (
	primary = 1
	standby = 2
	advance = 3

	fhNotExist = -1
	whatever   = -1
)

func ReBuildMap() {
	var count = 0
	parentInoList := vfs.V.Meta.GetPrentInodeList(meta.Background)
	edgeList := vfs.V.Meta.GetAllEdges(meta.Background)

	for _, Ino := range parentInoList {
		if count >= len(edgeList) {
			break
		}
		edgeListPart := edgeList[count:]
		for _, edge := range edgeListPart {
			if edge.Parent == Ino {
				var tmpName string
				if pName, ok := vfs.V.Inode2FileName.Get(strconv.FormatUint(uint64(Ino), 10)); ok {
					tmpName = pName.(string) + "/" + string(edge.Name)
				} else {
					tmpName = string(edge.Name)
				}
				vfs.V.Inode2FileName.Set(strconv.FormatUint(uint64(edge.Inode), 10), tmpName)
				count++
			} else {
				break
			}
		}
	}

	tmpMap := vfs.V.Inode2FileName.Items()
	for k, v1 := range tmpMap {
		uiK, err := strconv.ParseUint(k, 10, 64)
		if err == nil {
		vfs.V.FileName2Inode.Set(v1.(string), vfs.Ino(uiK))
		}else {
			logger.Errorf("cannot covert type(%v)",err)
		}
	}
}
func Init(vs *vfs.VFS, url, datadir string) {
	if datadir == "" {
		datadir = "data"
	}
	vfs.UsrConf = vfs.UserConfig{
		DataDir: datadir,
		MountDir: vs.Conf.Meta.MountPoint,
		MetaUrl: url,
	}

	vfs.V = vs
	if vfs.V == nil {
		os.Exit(1)
	}
	ReBuildMap()
	vfs.Ctx = &vfs.GrpcContext{}
	u, err := user.Current()
	if err == nil {
		uidNum,err:= strconv.Atoi(u.Uid)
		if err == nil {
			vfs.Ctx.SetUid(uint32(uidNum))
		} else {
			logger.Errorf("cannot convert type(%v)",err)
		}
		gidNum, err := strconv.Atoi(u.Gid)
		if err == nil {
			vfs.Ctx.SetGid(uint32(gidNum))
		}else {
			logger.Errorf("cannot convert type(%v)",err)
		}
	}else{
		logger.Errorf("cannot get info of current user(%v)",err)
	}

	fd2Inode = cmap.New()
	fd2File = cmap.New()

}

var logger = utils.GetLogger("juicefs")
var (
	fd2File  cmap.ConcurrentMap //local file handle,only used by standby
	fd2Inode cmap.ConcurrentMap //S3 file handle, used by primary and advance
)

func Ihe3Lseek(fd uint64, offset uint64, whence uint64) uint64 {
	attr := meta.Attr{}
	fdStr := strconv.FormatUint(fd, 10)
	inode, ok := fd2Inode.Get(fdStr)
	if ok {
		vfs.V.Meta.GetAttr(vfs.Ctx, inode.(meta.Ino), &attr)
		return attr.Length + offset
	} else {
		logger.Errorf("cannot find inode through given fd(%d)", fd)
		return 0
	}
}

func Ihe3Open(fileName string, fileFlages uint32, fileMode uint16, state uint8) (xfh int, xerrno int) {

	if vfs.UsrConf.DataDir != "" {
		fileName = vfs.UsrConf.DataDir + "/" + fileName
	}
	if find := strings.Contains(fileName,vfs.UsrConf.DataDir + "/" + meta.TblSpcName); find {
		var replaceStr string
		var remainStr string
		strList := strings.Split(fileName,"/")
		listLen := len(strList)
		for i,str:= range strList {
			if i < 3 {
				replaceStr = replaceStr + str + "/"
				continue
			}
			if i < listLen -1 {
			remainStr = remainStr + str + "/"
			}else {
				remainStr = remainStr + str
			}
		}

		if relativeStr,ok := meta.TblSpcSymToRelative.Get(replaceStr);ok {
			fileName = relativeStr.(string) + "/" + remainStr
		}
	
	}
    
	var (
		fe             *meta.Entry
		fh             uint64
		parentFileName string
		name           string
		topFlag        = false
	)

	if _, ok := vfs.V.FileName2Inode.Get(fileName); !ok { //create
		lastIndex := strings.LastIndex(fileName, "/")
		if lastIndex == -1 || lastIndex == 0 {
			topFlag = true
			name = fileName[:]
		} else {
			parentFileName = fileName[:lastIndex] //get parent dir
			name = fileName[lastIndex+1:]         //get file name
		}
		if _, ok := vfs.V.FileName2Inode.Get(parentFileName); !ok && !topFlag {
			xfh = fhNotExist
			xerrno = whatever
		}

		if fileFlages&uint32(os.O_RDWR) != 0 && fileFlages&uint32(os.O_CREATE) != 0 && fileFlages&uint32(os.O_EXCL) != 0 {
			if state == primary || state == advance { //primary and advance create file in S3
				if topFlag {
					fe, fh, e := vfs.V.Create(vfs.Ctx, 1, name, fileMode, 0, fileFlages)
					if e != 0 {
						xfh = fhNotExist
						xerrno = int(e)
						return
					}
					fd2Inode.Set(strconv.FormatUint(fh, 10), fe.Inode)
					vfs.V.FileName2Inode.Set(name, fe.Inode)
					vfs.V.Inode2FileName.Set(strconv.FormatUint(uint64(fe.Inode), 10), name)
					xfh = int(fh)
					xerrno = int(e)
				} else {
					ino, ok := vfs.V.FileName2Inode.Get(parentFileName)
					if !ok {
						logger.Errorf("cannot find inode through the given fileName(%s)", parentFileName)
						xfh = fhNotExist
						xerrno = int(whatever)
						return
					}
					fe, fh, e := vfs.V.Create(vfs.Ctx, ino.(vfs.Ino), name, fileMode, 0, fileFlages)
					if e != 0 {
						logger.Errorf("cannot create file in S3")
						xfh = fhNotExist
						xerrno = int(e)
						return
					}
					fd2Inode.Set(strconv.FormatUint(fh, 10), fe.Inode)
					vfs.V.FileName2Inode.Set(parentFileName+"/"+name, fe.Inode)
					vfs.V.Inode2FileName.Set(strconv.FormatUint(uint64(fe.Inode), 10), parentFileName+"/"+name)
					xfh = int(fh)
					xerrno = int(e)
				}

			} else { //standby creates file(pg_wal) locally
				f, err := os.Create(fileName)
				if err == nil {
					var edge *meta.Edge
					vfs.V.Meta.GetEgde(vfs.NewLogContext(meta.Background), edge) //find Inode from metadb
					fh = uint64(f.Fd())
					fd2File.Set(strconv.FormatUint(fh, 10), f)
					vfs.V.FileName2Inode.Set(parentFileName+"/"+name, edge.Inode)
					vfs.V.Inode2FileName.Set(strconv.FormatUint(uint64(fe.Inode), 10), parentFileName+"/"+name)
					xfh = int(fh)
					xerrno = 0
				} else {
					errno, e := strconv.Atoi(err.(syscall.Errno).Error())
					logger.Errorf("cannot create local file", err)
					if e != nil {
						xfh = fhNotExist
						xerrno = whatever
					} else {
						xfh = fhNotExist
						xerrno = errno
					}
				}
			}
		} else {
			xfh = fhNotExist
			xerrno = 2
		}
	} else if fileFlages&uint32(os.O_RDWR) != 0 && fileFlages&uint32(os.O_CREATE) != 0 && fileFlages&uint32(os.O_EXCL) != 0{
		lastIndex := strings.LastIndex(fileName, "/")
		if lastIndex == -1 || lastIndex == 0 {
			topFlag = true
			name = fileName[:]
		} else {
			parentFileName = fileName[:lastIndex] //get parent dir
			name = fileName[lastIndex+1:]         //get file name
		}
		if _, ok := vfs.V.FileName2Inode.Get(parentFileName); !ok && !topFlag {
			xfh = fhNotExist
			xerrno = whatever
		}

		if topFlag {
			fe, fh, e := vfs.V.Create(vfs.Ctx, 1, name, fileMode, 0, fileFlages)
			if e != 0 {
				xfh = fhNotExist
				xerrno = int(e)
				return
			}
			fd2Inode.Set(strconv.FormatUint(fh, 10), fe.Inode)
			vfs.V.FileName2Inode.Set(name, fe.Inode)
			vfs.V.Inode2FileName.Set(strconv.FormatUint(uint64(fe.Inode), 10), name)
			xfh = int(fh)
			xerrno = int(e)
		} else {
			ino, ok := vfs.V.FileName2Inode.Get(parentFileName)
			if !ok {
				logger.Errorf("cannot find inode through the given fileName(%s)", parentFileName)
				xfh = fhNotExist
				xerrno = int(whatever)
				return
			}
			fe, fh, e := vfs.V.Create(vfs.Ctx, ino.(vfs.Ino), name, fileMode, 0, fileFlages)
			if fe == nil{
				xfh = fhNotExist
				xerrno = whatever
				return
			}
			if e != 0 {
				logger.Errorf("cannot create file in S3")
				xfh = fhNotExist
				xerrno = int(e)
				return
			}
			fd2Inode.Set(strconv.FormatUint(fh, 10), fe.Inode)
			vfs.V.FileName2Inode.Set(parentFileName+"/"+name, fe.Inode)
			vfs.V.Inode2FileName.Set(strconv.FormatUint(uint64(fe.Inode), 10), parentFileName+"/"+name)
			xfh = int(fh)
			xerrno = int(e)
		}
	} else {//open
		if (state == primary) || (state == advance) { //primary or advance only opens files in S3
			ino, ok := vfs.V.FileName2Inode.Get(fileName)
			if !ok {
				logger.Errorf("cannot find inode through the given fileName(%s)", fileName)
				xfh = fhNotExist
				xerrno = int(whatever)
				return
			}
			fe, fh, e := vfs.V.Open(vfs.Ctx, ino.(vfs.Ino), fileFlages)
			if e == 0 {
				fd2Inode.Set(strconv.FormatUint(fh, 10), fe.Inode)
				xfh = int(fh)
				xerrno = int(e)
			} else {
				xfh = fhNotExist
				xerrno = int(e)
			}
		} else { //standby node logic
			f, err := os.OpenFile(fileName, int(fileFlages), fs.FileMode(fileMode))
			if err == nil { //local file exists
				fh = uint64(f.Fd())
				fd2File.Set(strconv.FormatUint(fh, 10), f)
				xfh = int(fh)
				xerrno = 0
			} else { //if local file does not exist, get it from S3
				ino, ok := vfs.V.FileName2Inode.Get(fileName)
				if !ok {
					logger.Errorf("cannot find inode through the given fileName(%s)", fileName)
					xfh = fhNotExist
					xerrno = int(whatever)
					return
				}
				fe, fh, e := vfs.V.Open(vfs.Ctx, ino.(vfs.Ino), fileFlages)
				if e == 0 {
					fd2Inode.Set(strconv.FormatUint(fh, 10), fe.Inode)
					xfh = int(fh)
					xerrno = int(e)
				} else {
					xfh = fhNotExist
					xerrno = int(e)

				}
			}
		}

	}
	return
}

func IwalRead(fh uint64, buf []byte, offset uint64) (n int) {
	if fh < vfs.MinFh {
		return IwalLocalRead(fh, buf, offset)
	} else {
		return IwalRestoreRead(fh, buf, offset)
	}
}

func Ihe3Write(fh uint64, buf []byte, offset uint64) int {

	if f, ok := fd2File.Get(strconv.FormatUint(fh, 10)); ok { 
		f.(*os.File).Write(buf)
		return len(buf)
	}
	if ino, ok := fd2Inode.Get(strconv.FormatUint(fh, 10)); ok {
		err := vfs.V.Write(vfs.Ctx, ino.(vfs.Ino), buf, offset, fh)
		if err == 0 {
			return len(buf)
		}
	}

	return -1
}

func IwalLocalRead(fh uint64, buf []byte, offset uint64) (n int) { 
	n = 0
	if f, ok := fd2File.Get(strconv.FormatUint(fh, 10)); ok { 
		f.(*os.File).Seek(int64(offset), 0)
		f.(*os.File).Name()
		tmp, err := f.(*os.File).Read(buf)
		if err == nil {
			n = tmp
		}
		return
	}
	return
}

func IwalRestoreRead(fh uint64, buf []byte, offset uint64) (n int) { 
	n = 0
	if ino, ok := fd2Inode.Get(strconv.FormatUint(fh, 10)); ok {
		ctx := vfs.NewLogContext(meta.Background)
		tmp, err := vfs.V.Read(ctx, ino.(vfs.Ino), buf, offset, fh)
		if err == 0 {
			n = tmp
		}
		return
	}
	return
}

func IdataRead(fh uint64, offset uint64, lastLsn uint64) []byte { //page + wal
	inode, ok := fd2Inode.Get(strconv.FormatUint(fh, 10))
	if !ok {
		logger.Errorf("cannot find inode through the given fd(%d)", fh)
		return nil
	}

	bytes, n, err := vfs.V.He3Read(vfs.Ctx, inode.(vfs.Ino), offset, fh, lastLsn)
	if err != 0 {
		logger.Errorf("read err is %s,n is %d \n", err, n)
	}
	return bytes
}

func Ihe3Unlink(fileName string) (stat int) {
	stat = 1
	fullPath := fileName
	_, err := os.Stat(fullPath) 
	if os.IsNotExist(err) {
		err := os.Remove(fullPath)
		if os.IsExist(err) {
			stat = -1
			return
		}
		return
	}

	if _, ok := vfs.V.FileName2Inode.Get(fileName); ok { 
		lastIndex := strings.LastIndex(fileName, "/")
		parentFileName := fileName[:lastIndex]
		name := fileName[lastIndex+1:]         
		ctx := vfs.NewLogContext(meta.Background)
		ino, ok := vfs.V.FileName2Inode.Get(parentFileName)
		if !ok {
			logger.Errorf("cannot find inode through the given fileName(%s)", fileName)
			stat = -1
			return
		}
		err := vfs.V.Unlink(ctx, ino.(vfs.Ino), name)
		if err != 0 {
			stat = -1
		}
	}
	return
}

func Ihe3Close(fh uint64) int {

	if f, ok := fd2File.Get(strconv.FormatUint(fh, 10)); ok {
		f.(*os.File).Close()
		fd2File.Remove(strconv.FormatUint(fh, 10))
		return 0
	}

	if ino, ok := fd2Inode.Get(strconv.FormatUint(fh, 10)); ok {
		vfs.V.ReleaseHandler(ino.(vfs.Ino), fh)
		fd2Inode.Remove(strconv.FormatUint(fh, 10))
		return 0
	}
	return -1

}

func IunlinkLocalWalFile(localWalName string) int {
	paths := strings.Split(localWalName, "/")
	if len(paths) <= 0 {
		return -1
	}
	fname := paths[len(paths)-1]
	err := vfs.XlogUnlink(fname)
	if err == nil {
		rerr := os.Remove(localWalName)
		if rerr != nil {
			logger.Errorf("remove local wal file(%v) filed:%v", localWalName, rerr)
			return -1
		}
	}

	return 0
}

func Ihe3Truncate(fh uint64, offset uint64) int32 {
	var err syscall.Errno
	if ino, ok := fd2Inode.Get(strconv.FormatUint(fh, 10)); ok {
		var attr = &meta.Attr{}
		err = vfs.V.Truncate(vfs.Ctx, ino.(vfs.Ino), int64(offset), 1, attr)
	}
	return int32(err)
}

func Ihe3Fsync(fh uint64) int32 {
	var err syscall.Errno
	if ino, ok := fd2Inode.Get(strconv.FormatUint(fh, 10)); ok {
		err = vfs.V.Fsync(vfs.Ctx, ino.(vfs.Ino), 1, fh)
	}
	return int32(err)
}
